﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-21 16:10
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BassDrop.Core.Common.Media.Playlists;
using BassDrop.Core.Common.Plugins;

namespace BassDrop.Core.Common.Media
{
	/// <summary>
	/// Abstract base class for media players
	/// 
	/// Declares an interface for audio playback
	/// </summary>
	public abstract class MediaDecoderBase : PluginObjectBase, INotifyPropertyChanged, IDisposable
	{
		#region Events

		/// <summary>
		/// Event for changes to the Position property
		/// </summary>
		public event PropertyChangedEventHandler PositionChanged;

		/// <summary>
		/// Event for changes to the Track property
		/// </summary>
		public event PropertyChangedEventHandler TrackChanged;

		#endregion Events

		#region Fields and properties

		/// <summary>
		/// The interval in which the current position is refreshed by the player in milliseconds.
		/// </summary>
		private int m_progressUpdateInterval;

		/// <summary>
		/// Timer to periodically update progress
		/// </summary>
		private Timer m_progressUpdateTimer;

		/// <summary>
		/// The currently played media
		/// </summary>
		private PlaylistItemMediaTrack m_track;
		/// <summary>
		/// Gets or sets the currently played media.
		/// Setting it resets the position.
		/// </summary>
		public PlaylistItemMediaTrack Track
		{
			get
			{
				return m_track;
			}

			set
			{
				if ( m_track != value )
				{
					m_track = value;

					if ( TrackChanged != null )
					{
						TrackChanged( this, new PropertyChangedEventArgs( "Track" ) );
					}

					OnPropertyChanged( "Track" );

					Position = null;
				}
			}
		}

		/// <summary>
		/// Used to lock the Position property for update
		/// </summary>
		private object m_position_lock = new object();
		/// <summary>
		/// The position inside the track in milliseconds
		/// 
		/// Null if the position is indeterminate
		/// (eg. the playback is stopped, or seeking is not supported by the current plugin)
		/// </summary>
		private long? m_position;
		/// <summary>
		/// Gets or sets the position inside the track in milliseconds
		/// 
		/// Null if the position is indeterminate
		/// (eg. the playback is stopped, or seeking is not supported by the current plugin)
		/// </summary>
		public long? Position
		{
			get
			{
				lock ( m_position_lock )
				{
					return m_position;
				}
			}

			set
			{
				lock ( m_position_lock )
				{
					if ( m_position == value )
					{
						return;
					}

					if ( value == null )
					{
						m_position = value;
					}
					else
					{
						if ( Track != null && Track.Length != null )
						{
							long pos = (long) value;
							long len = (long) Track.Length;

							// TODO: túl nagy pos értékek
							if ( pos < 0 || pos > len )
							{
								throw new ArgumentException( value.ToString(), "Position" );
							}

							m_position = pos;
						}
						// TODO: else exception?
					}

					if ( PositionChanged != null )
					{
						PositionChanged( this, new PropertyChangedEventArgs( "Position" ) );
					}

					OnPropertyChanged( "Position" );
				}
			}
		}

		/// <summary>
		/// Gets or sets the position inside the track in percents
		/// 
		/// Throws an ArgumentException if the value isn't between 0 and 100 or null
		/// 
		/// Null if the position is indeterminate
		/// (eg. the playback is stopped, or seeking is not supported by the current plugin)
		/// </summary>
		public double? PercentPosition
		{
			get
			{
				lock ( m_position_lock )
				{
					if ( Position == null || Track == null || Track.Length == null )
					{
						return null;
					}
					else
					{
						double pos = (double) Position;
						double len = (double) Track.Length;

						return Math.Min( 100, pos / len * 100.0 );
					}
				}
			}

			set
			{
				lock ( m_position_lock )
				{
					if ( value == null )
					{
						Position = null;
					}
					else if ( (double) value >= 0.0 && (double) value <= 100.0 )
					{
						if ( Track != null && Track.Length != null )
						{
							double pos = (double) value;
							double len = (double) Track.Length;

							Position = (long) (len * (pos / 100.0));
						}
						// TODO: else exception?
					}
					else
					{
						throw new ArgumentException( value.ToString(), "PercentPosition" );
					}
				}
			}
		}

		#endregion Fields and properties

		/// <summary>
		/// Protected constructor to wire in local events
		/// </summary>
		/// <param name="pluginDescriptor">The descriptor object of the plugin owning this object</param>
		/// <param name="instanceName">the name of this plugin instance</param>
		/// <param name="refreshInterval">
		///		The interval in which the current position is refreshed by the player in milliseconds.
		///		Defaults to 1000.
		///	</param>
		protected MediaDecoderBase( BassDropPluginBase pluginDescriptor, string instanceName, int refreshInterval = 1000 )
			: base( pluginDescriptor, instanceName )
		{
			TrackChanged += MediaDecoderBase_TrackChanged;
			PositionChanged += MediaDecoderBase_PositionChanged;
			m_progressUpdateInterval = refreshInterval;

			if ( m_progressUpdateInterval < 1 )
			{
				m_progressUpdateInterval = 1;
			}
		}

		/// <summary>
		/// Use this from inside this class to update the position.
		/// Same as setting the Position property, except it doesn't call the
		/// MediaDecoderBase_PositionChanged event handler.
		/// If the position is currently being changed from the outside it does nothing.
		/// </summary>
		/// <param name="newPos">the new position</param>
		protected void UpdatePosition( long? newPos )
		{
			if ( Monitor.TryEnter( m_position_lock ) )
			{
				try
				{
					PositionChanged -= MediaDecoderBase_PositionChanged;
					Position = newPos;
					PositionChanged += MediaDecoderBase_PositionChanged;
				}
				finally
				{
					Monitor.Exit( m_position_lock );
				}
			}
			// else throw away the value as the position was currently being changed from the outside
		}

		#region Playback controls

		/// <summary>
		/// Starts playing the selected track from the selected position
		/// </summary>
		public void Play()
		{
			if ( Position == null )
			{
				Position = 0;
			}

			StartPlayback();
			Track.CurrentPlayState = PlaylistItemBase.PlayState.Playing;

			if ( m_progressUpdateTimer == null )
			{
				m_progressUpdateTimer = new Timer( RefreshPosition, null, m_progressUpdateInterval, m_progressUpdateInterval );
			}
		}

		/// <summary>
		/// Pauses playing the track
		/// </summary>
		public void Pause()
		{
			if ( Position == null )
			{
				Position = 0;
			}

			PausePlayback();
			Track.CurrentPlayState = PlaylistItemBase.PlayState.Paused;

			if ( m_progressUpdateTimer == null )
			{
				m_progressUpdateTimer = new Timer( RefreshPosition, null, m_progressUpdateInterval, m_progressUpdateInterval );
			}
		}

		/// <summary>
		/// Stops playing the track and resets Position to null
		/// </summary>
		public void Stop()
		{
			StopPlayback();
			Position = null;

			if ( Track != null )
			{
				Track.CurrentPlayState = PlaylistItemBase.PlayState.Stopped;
			}

			if ( m_progressUpdateTimer != null )
			{
				m_progressUpdateTimer.Dispose();
				m_progressUpdateTimer = null;
			}
		}

		#endregion Playback controls

		#region Abstract methods

		/// <summary>
		/// Parses metadata from the file for the passed Track and returns it
		/// in a MediaTrack.Metadata object
		/// </summary>
		/// <param name="track">the Track to parse data from</param>
		/// <returns></returns>
		public abstract Task<MediaTrack.Metadata> GetMetadataForTrack( MediaTrack track );

		/// <summary>
		/// Starts playing the selected track from the selected position
		/// </summary>
		protected abstract void StartPlayback();

		/// <summary>
		/// Pauses playing the track
		/// </summary>
		protected abstract void PausePlayback();

		/// <summary>
		/// Stops playing the track
		/// </summary>
		protected abstract void StopPlayback();

		/// <summary>
		/// Called periodically to refresh the Position property from the player
		/// </summary>
		/// <param name="state">always null</param>
		protected abstract void RefreshPosition( object state );

		/// <summary>
		/// Called when the position is changed from the outside
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected abstract void MediaDecoderBase_PositionChanged( object sender, PropertyChangedEventArgs e );

		/// <summary>
		/// Called when the track is replaced with a new one
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected abstract void MediaDecoderBase_TrackChanged( object sender, PropertyChangedEventArgs e );

		#endregion Abstract methods

		#region IDisposable Members

		/// <summary>
		/// Releases all resources used by this object
		/// </summary>
		public virtual void Dispose()
		{
			Stop();
		}

		#endregion
	}
}
